--[[
    SpamBayes for WoW, a lightweight bayesian antispam for World of Warcraft
    Copyright @ 2010 Arnaud Dovi (Merah@AthelLoren, ad@heapoverflow.com)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
]]

--[[
$Rev:: 40                    $:  Revision of last commit
$Author:: arnaud             $:  Author of last commit
$Date:: 2012-09-02 21:46:32 #$:  Date of last commit
$WoW:: 5.0.5.16030           $:  Client of the last tests
]]

----------------- GLOBALS ----------------

------------------------------------------

local lastReport, LINEID, lineID, db, cdb, xml, event, orig_FriendsFrame_ShowDropdown
local SpamBayes                       = SpamBayes
local getprob                         = SpamBayes.chi2.getprob
local firstLoad                       = true
local isEventRegistered               = false
local isUpdateNotified                = false
local trainTable                      = {} -- BLIZZARD ToDo: something like GetMessage(lineId) would be useful
local repTable                        = {} -- to report the spammer only once per session and one line only

local type                            = type
local date                            = date
local tonumber                        = tonumber
local tostring                        = tostring

local CHAT_AFK_GET                    = CHAT_AFK_GET                  -- "%s is Away:\32"; ###sender msg
local CHAT_BATTLEGROUND_GET           = CHAT_BATTLEGROUND_GET         -- "|Hchannel:BATTLEGROUND|h[Battleground]|h %s:\32"; ###sender msg
local CHAT_BATTLEGROUND_LEADER_GET    = CHAT_BATTLEGROUND_LEADER_GET  -- "|Hchannel:BATTLEGROUND|h[Battleground Leader]|h %s:\32"; ###sender msg
local CHAT_CHANNEL_GET                = CHAT_CHANNEL_GET              -- "%s:\32"; ###channel sender msg
local CHAT_DND_GET                    = CHAT_DND_GET                  -- "%s does not wish to be disturbed:\32"; ###sender msg
local CHAT_EMOTE_GET                  = CHAT_EMOTE_GET                -- "%s\32"; ###sender msg
local CHAT_SAY_GET                    = CHAT_SAY_GET                  -- "%s says:\32"; ###sender msg
local CHAT_WHISPER_GET                = CHAT_WHISPER_GET              -- "%s whispers:\32"; ###sender msg
local CHAT_YELL_GET                   = CHAT_YELL_GET                 -- "%s yells:\32"; ###sender msg

local MINOR_VERSION                   = tonumber(("$Rev: 40 $"):match("%d+"))
SpamBayes.MINOR_VERSION               = SpamBayes.MINOR_VERSION == nil and MINOR_VERSION or (MINOR_VERSION > SpamBayes.MINOR_VERSION and MINOR_VERSION or SpamBayes.MINOR_VERSION)

-- 90% or greater   = SPAM
-- 20% or lower     = HAM
-- anything between = UNSURE
local SPAM_CUTOFF                     = 0.90
local HAM_CUTOFF                      = 0.20
local COLOR_SPAM                      = "|cffff0000"
local COLOR_HAM                       = "|cff00ff00"
local COLOR_UNSURE                    = "|cffffffff"
local COLOR_UNTRAIN                   = "|cff808080"
local COLOR_STOP                      = "|r"

local isSafeCustomChannel = { -- put here channel names from addons in lower case or multiple channels with 3 first letters in normal case and a leading "_" symbol
    ["_Crb"] = true, -- Carbonite
}

local isAnotherSpamAddOnLoaded = {
    ["SpamMeNot"] = true,
    ["BadBoy"] = true,
    ["SpamSentry"] = true
}

local function D(text)
    print("|cff808080[SpamBayes] : "..text.."|r")
end

local function isAddOnEnabled()
    return db["isAddOnEnabled"] == true
end

local function linksEnabled()
    return db["linksEnabled"] == true
end

local function autoreportEnabled()
    return db["autoreportEnabled"] == true
end

local function hiderpEnabled()
    return db["hiderpEnabled"] == true
end

local hardcoded = { -- removing a line in this table also disables the chat message type from being scanned
    CHAT_MSG_AFK                 = CHAT_AFK_GET,
    CHAT_MSG_BATTLEGROUND        = CHAT_BATTLEGROUND_GET,
    CHAT_MSG_BATTLEGROUND_LEADER = CHAT_BATTLEGROUND_LEADER_GET,
    CHAT_MSG_CHANNEL             = CHAT_CHANNEL_GET,
    CHAT_MSG_DND                 = CHAT_DND_GET,
    CHAT_MSG_EMOTE               = CHAT_EMOTE_GET,
    CHAT_MSG_SAY                 = CHAT_SAY_GET,
    CHAT_MSG_WHISPER             = CHAT_WHISPER_GET,
    CHAT_MSG_YELL                = CHAT_YELL_GET,
    CHAT_MSG_SYSTEM              = CHAT_MSG_SYSTEM
}

local function updateCounters()
    xml.setinfo:SetText(("Total messages trained : %d Spam, %d Ham\nMessages classified : %d Spam, %d Ham, %d Unsure\nLast report : %s %s"):format(cdb.nspam, cdb.nham, cdb.cspam, cdb.cham, cdb.cunsure, lastReport or "-", ((autoreportEnabled() and cdb.nspam >= 10 and cdb.nham >= 10) or not autoreportEnabled()) and "" or "(train a minimum of 10 ham and spam to enable auto reports)"))
end

local function string_split(text, sep, pattern)
    local sep, fields = sep or " ", {}
    local patt = pattern or ("([^%s]+)"):format(sep)
    text:gsub(patt, function(c) fields[#fields+1] = c:lower() end)
    return fields
end

local function tokenize(msg)
    return string_split(tostring(msg), _, "([^%s%p%c]+)") -- obfuscations removal
end

local function train(isSpam, unLearn)
    local tokens = trainTable[LINEID] ~= nil and tokenize(trainTable[LINEID]) or D("Unexpected error while "..(unLearn and "unlearning" or "training").." as "..(isSpam and "SPAM" or "HAM").." (please retry this is a known client bug")
    if type(tokens) == "table" then
        local success
        if unLearn then
            success = SpamBayes.chi2.unlearn(tokens, isSpam)
        else
            SpamBayes.chi2.learn(tokens, isSpam)
        end
        D((success == 0 and "Error with " or "Success with ")..(unLearn and "unlearn" or "train").." one more "..(isSpam and "SPAM" or "HAM")..(success == 0 and " (the phrase was never learned as "..(isSpam and "spam" or "ham").." or slightly changed)" or ""))
        updateCounters()
    end
end


local function GetFormatString(event, msg, player, channelName, lineId, chatTarget, prob, isHam, isSpam)
    local ctype     = event:match("^CHAT_MSG_(.*)")
    local sender    = (type(player) == "string" and player:len() > 0) and ("|Hplayer:%s:%s:%s:%s|h[%s]|h"):format(player, lineId, ctype, chatTarget, player) or nil
    local timestamp = date("|cff808080["..(not LOCALE_enUS and "%H:%M" or "%I:%M %p").."]|r ")
    local prob_l    = ("%.1f"):format(prob * 100)
    local prob_t    = (prob_l:len() == 4 and "[%s %%] " or (prob_l:len() == 5 and prob_l == "100.0") and "[ 100 %%] " or "[0%s %%] "):format(prob_l)
    local channel   = (type(channelName) == "string" and channelName:len() > 0) and ("[%s] "):format(channelName) or nil
    local color     = isHam and COLOR_HAM or (isSpam and COLOR_SPAM or COLOR_UNSURE)
    local str       = ("%s%s%s%s"):format(color, prob_t, channel and channel or "", sender and sender or "")
    msg             = msg:gsub("|r", color)
    return (timestamp..hardcoded[event].." %s|r"):format(str, msg)
end

local function filter(_, event, msg, player, _, formattedChannelName, _, _, _, chatTarget, channelName, _, lineId)
    if lineID == lineId then return end
    lineID = lineId
    local isChannel              = type(channelName) == "string" and channelName:len() > 0
    local isSafe                 = isChannel and isSafeCustomChannel[channelName:lower()] == true or isSafeCustomChannel["_"..channelName:sub(0, 3)] == true or false
    local isKnown                = UnitIsInMyGuild(player) or UnitInRaid(player) or UnitInParty(player) or UnitName("player") == player or false
    if isSafe or isKnown then return false end
    local prob                   = getprob(tokenize(msg))
    trainTable[tonumber(lineId)] = msg
    local isHam                  = prob <= HAM_CUTOFF
    local isSpam                 = prob >= SPAM_CUTOFF
    xml.log:AddMessage(GetFormatString(event, tostring(msg), player, formattedChannelName, lineId, chatTarget, prob, isHam, isSpam))
    if isHam then
        cdb.cham    = cdb.cham + 1
    elseif isSpam then
        if autoreportEnabled() and CanComplainChat(lineId) and (not repTable[player] and cdb.nspam >= 15 and cdb.nham >= 30) then
            repTable[player] = true
            --ComplainChat(lineId)
            ReportPlayer("spam", lineId)
            lastReport = player
        end
        cdb.cspam   = cdb.cspam + 1
    else
        cdb.cunsure = cdb.cunsure + 1
    end
    updateCounters()
    return isSpam and true or false
end

local buttons
local function newFriendsFrame_ShowDropdown(name, _, lineid, ...)
    buttons = buttons or {
        [0] = {
            text         = "|cfffed100SpamBayes|r",
            notCheckable = 1,
            notClickable = 1
        },
        [1] = {
            text         = COLOR_HAM.."Train HAM"..COLOR_STOP,
            func         = function() train(false) end,
            notCheckable = 1
        },
        [2] = {
            text         = COLOR_SPAM.."Train SPAM"..COLOR_STOP,
            func         = function() train(true) end,
            notCheckable = 1
        },
        [3] = {
            text         = COLOR_UNTRAIN.."Unlearn HAM"..COLOR_STOP,
            func         = function() train(false, true) end,
            notCheckable = 1
        },
        [4] = {
            text         = COLOR_UNTRAIN.."Unlearn SPAM"..COLOR_STOP,
            func         = function() train(true, true) end,
            notCheckable = 1
        }
    }
    if name == UnitName("player") then return end
    orig_FriendsFrame_ShowDropdown(name, _, lineid, ...)
    if type(lineid) ~= "string" then return end
    LINEID = tonumber(lineid)
    for i = 0, #buttons do
        UIDropDownMenu_AddButton(buttons[i])
    end
end

local function Install_or_Remove_Hooks(forceOff)
    if not forceOff and isAddOnEnabled() then
        if not isEventRegistered then
            for event, _ in pairs(hardcoded) do
                if event ~= "CHAT_MSG_SYSTEM" then
                    ChatFrame_AddMessageEventFilter(event, filter)
                else
                    ChatFrame_AddMessageEventFilter(event, function(_, _, msg) return hiderpEnabled() and msg == COMPLAINT_ADDED end)
                end
            end
            isEventRegistered = true
            FriendsFrame_ShowDropdown = newFriendsFrame_ShowDropdown
        end
    else
        if isEventRegistered then
            for event, _ in pairs(hardcoded) do
                if event ~= "CHAT_MSG_SYSTEM" then
                    ChatFrame_RemoveMessageEventFilter(event, filter)
                else
                    ChatFrame_RemoveMessageEventFilter(event, function(_, _, msg) return hiderpEnabled() and msg == COMPLAINT_ADDED end)
                end
            end
            isEventRegistered = false
            if orig_FriendsFrame_ShowDropdown then FriendsFrame_ShowDropdown = orig_FriendsFrame_ShowDropdown end
        end
    end
end

local function antiAntiSpam(c)
    D(("is disabled because it has detected another spam addon running (%s) see here why: http://goo.gl/4KEX7"):format(c))
    DisableAddOn("SpamBayes")
end

local function updateNotify()
    if not isUpdateNotified and SPAMBAYESDB and type(SPAMBAYESDB.isUpdate) == "number" and IsResting() then
        if SPAMBAYESDB.isUpdate > SpamBayes.MINOR_VERSION then
            print(("|cffffff00SpamBayes : a newer build is available (r%d)"):format(SPAMBAYESDB.isUpdate))
            UIErrorsFrame:AddMessage(("|cffffff00SpamBayes : a newer build is available (r%d)"):format(SPAMBAYESDB.isUpdate))
            isUpdateNotified = true
        end
        SPAMBAYESDB.isUpdate = false
    end
end

local function OnEvent(_, ev, addon, v)
    if ev == "PLAYER_ENTERING_WORLD" or ev == "PLAYER_UPDATE_RESTING" then
        if IsInGuild()                  then SendAddonMessage("SpamBayesREV", SpamBayes.MINOR_VERSION, "GUILD") end
        if UnitInBattleground("player") then SendAddonMessage("SpamBayesREV", SpamBayes.MINOR_VERSION, "BATTLEGROUND") end
        if UnitInRaid("player")         then SendAddonMessage("SpamBayesREV", SpamBayes.MINOR_VERSION, "RAID") end
        if UnitInParty("player")        then SendAddonMessage("SpamBayesREV", SpamBayes.MINOR_VERSION, "PARTY") end
        if not isUpdateNotified then updateNotify() end
        return
    end
    if ev == "CHAT_MSG_ADDON" and addon == "SpamBayesREV" and not isUpdateNotified then
        if tonumber(v) > SpamBayes.MINOR_VERSION and SPAMBAYESDB then
            SPAMBAYESDB.isUpdate = type(SPAMBAYESDB.isUpdate) ~= "number" and tonumber(v) or (tonumber(v) < SPAMBAYESDB.isUpdate and SPAMBAYESDB.isUpdate or tonumber(v))
            updateNotify()
        end
        return
    end
    if isAnotherSpamAddOnLoaded[addon] then
        antiAntiSpam(addon)
        Install_or_Remove_Hooks(true)
        if xml and xml.check then xml.check:SetScript("OnClick", nil) end
        return
    end
    if addon ~= "SpamBayes" then return end
    if not firstLoad then return end
    firstLoad = false
    RegisterAddonMessagePrefix("SpamBayesREV")
    local mname
    for i = 1, GetNumAddOns() do
        if IsAddOnLoaded(i) then
            mname = GetAddOnInfo(i)
            if isAnotherSpamAddOnLoaded[mname] then
                antiAntiSpam(mname)
                return
            end
        end
    end
    xml = SpamBayes.xml
    local function toggle()
        if xml.frame:IsVisible() then
            xml.aframe:Hide()
            xml.oframe:Hide()
            xml.hframe:Hide()
            return xml.frame:Hide()
        end
        return xml.frame:Show()
    end
    local LDB              = LibStub("LibDataBroker-1.1")
    SPAMBAYESDB            = SPAMBAYESDB              or { isAddOnEnabled = true, linksEnabled = false, autoreportEnabled = false , hiderpEnabled = false, isUpdate = false}
    db                     = SPAMBAYESDB
    db.worddb              = SPAMBAYESDB.worddb       or {}
    db.classifierdb        = SPAMBAYESDB.classifierdb or { nham = 0, nspam = 0, cspam = 0, cham = 0, cunsure = 0, probcache = {}}
    cdb                    = db.classifierdb
    SLASH_SPAMBAYES1       = "/spambayes"
    SLASH_SPAMBAYES2       = "/sb"
    SlashCmdList.SPAMBAYES = toggle
    LDB:NewDataObject("SpamBayes", {
        type    = "launcher",
        text    = "SpamBayes",
        icon    = "Interface\\AddOns\\SpamBayes\\textures\\spambayes_logo",
        OnClick = toggle
    })
    orig_FriendsFrame_ShowDropdown = FriendsFrame_ShowDropdown
    xml.init(db)
    xml.init = nil
    Install_or_Remove_Hooks(false)
    D(isAddOnEnabled() and "ON" or "OFF")
end

SpamBayes.isAddOnEnabled = isAddOnEnabled
SpamBayes.linksEnabled   = linksEnabled
SpamBayes.rpEnabled      = autoreportEnabled
SpamBayes.lastrp         = lastReport
SpamBayes.HiderpEnabled  = hiderpEnabled
SpamBayes.shutdown       = Install_or_Remove_Hooks

event = CreateFrame("Frame")
event:SetScript("OnEvent", OnEvent)
event:RegisterEvent("ADDON_LOADED")
event:RegisterEvent("PLAYER_ENTERING_WORLD")
event:RegisterEvent("CHAT_MSG_ADDON")
event:RegisterEvent("PLAYER_UPDATE_RESTING")
